/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.pentaho.di.trans.steps.webservices.wsdl;

import org.w3c.dom.Element;
import javax.xml.namespace.QName;

/**
 * WSDL operation parameter abstraction.
 */
public final class WsdlOpParameter extends WsdlOpReturnType implements java.io.Serializable {

    private static final long serialVersionUID = 1L;

    public static class ParameterMode {
        private String mode;
        public static final ParameterMode IN = new ParameterMode("IN");
        public static final ParameterMode OUT = new ParameterMode("OUT");
        public static final ParameterMode INOUT = new ParameterMode("INOUT");
        public static final ParameterMode UNDEFINED = new ParameterMode("UNDEFINED");
        private ParameterMode(String mode) { this.mode = mode;}
        public String toString() { return mode;}
    }

    private QName _name;
    private ParameterMode _mode;
    private boolean _isHeader;
    private boolean _elementFormQualified;

    /**
     * Create operation parameters whose types do not need to
     * be unwrapped. Typically used for RPC style
     * parameters but also may be used for DOCUMENT style when parameter style is BARE.
     *
     * @param name              QName of the parameter.
     * @param xmlType           XML type of the parameter.
     * @param schemaTypeElement The type element from the schema for this parameter,
     *                          will be null if xmlType is a built-in schema type.
     * @param wsdlTypes         Wsdl types abstraction.
     */
    WsdlOpParameter(String name, QName xmlType, Element schemaTypeElement,
                    WsdlTypes wsdlTypes) {

        setName(name, wsdlTypes);
        _xmlType = xmlType;
        _itemXmlType = getArrayItemType(schemaTypeElement, wsdlTypes);
        _isArray = _itemXmlType != null;
        _isHeader = false;
        _mode = ParameterMode.UNDEFINED;
    }

    /**
     * Create a new WsdlOpParameter for a simple schema type.  For pararmeters
     * of simple type, the name of the parameter corresponds to the name of the
     * message part which defines it.
     *
     * @param name      Name of the attribute, if a namespace ref is included it will be resolved.
     * @param e         The schema element which defines the XML type of this attribute.
     * @param wsdlTypes Wsdl types abstraction.
     */
    WsdlOpParameter(String name, Element e, WsdlTypes wsdlTypes) {

        this(e, wsdlTypes);
        setName(name, wsdlTypes);
    }

    /**
     * Create a new WsdlOpParameter for a complex type.
     *
     * @param e         The schema element which defines the XML type of this attribute.
     * @param wsdlTypes Wsdl types abstraction.
     */
    WsdlOpParameter(Element e, WsdlTypes wsdlTypes) {

        _mode = ParameterMode.UNDEFINED;
        _isArray = isArray(e);
        _isHeader = false;

        if (e.hasAttribute(WsdlUtils.NAME_ATTR) && e.hasAttribute(WsdlUtils.ELEMENT_TYPE_ATTR)) {
            setName(e.getAttribute(WsdlUtils.NAME_ATTR), wsdlTypes);
            _xmlType = wsdlTypes.getTypeQName(e.getAttribute(WsdlUtils.ELEMENT_TYPE_ATTR));
        }
        else if (e.hasAttribute(WsdlUtils.ELEMENT_REF_ATTR)) {
            _xmlType = wsdlTypes.getTypeQName(e.getAttribute(WsdlUtils.ELEMENT_REF_ATTR));
            _name = new QName("", _xmlType.getLocalPart());
        }
        else if (e.hasAttribute(WsdlUtils.NAME_ATTR)) {
            setName(e.getAttribute(WsdlUtils.NAME_ATTR), wsdlTypes);
            _xmlType = getElementType(e, wsdlTypes);
        }
        else {
            throw new RuntimeException("invalid element: " + e.getNodeName());
        }

        // check to see if the xml type of this element is an array type
        Element t = wsdlTypes.findNamedType(_xmlType);
        if (t != null && WsdlUtils.COMPLEX_TYPE_NAME.equals(t.getLocalName())) {
            _itemXmlType = getArrayItemType(t, wsdlTypes);
            _isArray = _itemXmlType != null;
            if (_itemXmlType != null)
            {
                _itemComplexType = wsdlTypes.getNamedComplexTypes().getComplexType(_itemXmlType.getLocalPart());
            }
        }
    }

    /**
     * Get the name of this parameter.
     *
     * @return QName.
     */
    public QName getName() {
        return _name;
    }

    /**
     * Get the mode of this parameter.
     *
     * @return ParameterMode
     */
    public ParameterMode getMode() {
        return _mode;
    }

    /**
     * Is this paramter's name element form qualified?
     *
     * @return True if element form qualified.
     */
    public boolean isNameElementFormQualified() {
        return _elementFormQualified;
    }

    /**
     * Is this parameter a SOAP header parameter?
     *
     * @return true if it is.
     */
    public boolean isHeader() {
        return _isHeader;
    }

    /**
     * Mark this parameter as a SOAP header parameter.
     */
    protected void setHeader() {
        _isHeader = true;
    }

    /**
     * Set the mode of this parameter (IN/OUT/INOUT).
     *
     * @param mode the mode to set.
     */
    protected void setMode(ParameterMode mode) {
        _mode = mode;
    }

    /**
     * Set the name of this parameter.
     *
     * @param name      parameter name.
     * @param wsdlTypes Wsdl types abstraction.
     */
    protected void setName(String name, WsdlTypes wsdlTypes) {
        _name = wsdlTypes.getTypeQName(name);
        _elementFormQualified = wsdlTypes.isElementFormQualified(_name.getNamespaceURI());
    }

    /**
     * Does this element represent an array type?
     *
     * @param e Element to check.
     * @return true if this element represents an array type.
     */
    private boolean isArray(Element e) {

        if (e.hasAttribute(WsdlUtils.MAXOCCURS_ATTR)
                && !"1".equals(e.getAttribute(WsdlUtils.MAXOCCURS_ATTR))) {
            return true;
        }

        if (e.hasAttribute(WsdlUtils.MINOCCURS_ATTR)) {
            String minOccurs = e.getAttribute(WsdlUtils.MINOCCURS_ATTR);
            try {
                int i = Integer.parseInt(minOccurs);
                if (i > 1) {
                    return true;
                }
            }
            catch (NumberFormatException nfe) {
                // don't fail - just means minOccurs isn't set
            }
        }
        return false;
    }

    /**
     * Get the xml type of an element.
     *
     * @param element   Element to determine the xml type of.
     * @param wsdlTypes Wsdl types abstraction.
     * @return QName of the element's xml type, null if type cannot be determined.
     */
    private QName getElementType(Element element, WsdlTypes wsdlTypes) {

        /* Get the type of an element when the element is in the form of:
            <element name="foo">
              <complexType>
               <sequence>
                 <element name="bar" ref="s:schema"/>
               </sequence>
             </complexType>
            </element>

            or

            <element name="foo">
              <complexType>
               <sequence>
                 <any/>
               </sequence>
             </complexType>
            </element>

         This code is extremely brittle, this is a construct used
         by dot net when DataTypes are employed.  The code will need to be enhanced
         if other samples of this construct arise.
         */
        Element child;
        if ((child = DomUtils.getChildElementByName(element, WsdlUtils.COMPLEX_TYPE_NAME)) != null) {
            if ((child = DomUtils.getChildElementByName(child, WsdlUtils.SEQUENCE_TAG_NAME)) != null) {
                Element childElement = DomUtils.getChildElementByName(child, WsdlUtils.ELEMENT_NAME);
                if (childElement != null) {
                    if (child.hasAttribute(WsdlUtils.ELEMENT_REF_ATTR)) {
                        return wsdlTypes.getTypeQName(child.getAttribute(WsdlUtils.ELEMENT_REF_ATTR));
                    }
                    else if (child.hasAttribute(WsdlUtils.ELEMENT_TYPE_ATTR)) {
                        return wsdlTypes.getTypeQName(child.getAttribute(WsdlUtils.ELEMENT_TYPE_ATTR));
                    }
                }
                else if ((childElement = DomUtils.getChildElementByName(child, WsdlUtils.ANY_TAG_NAME)) != null) {
                    return new QName(childElement.getNamespaceURI(), childElement.getLocalName());
                }
            }
        }
        else {
            // no children / no type map to 'any'
            return new QName("http://www.w3.org/2001/XMLSchema", "any");
        }
        return new QName("http://www.w3.org/2001/XMLSchema", "String"); // throw new RuntimeException("Cannot determine type of element: " + getName());
    }

    /**
     * This method differs from the isArray(e) method in that it is checking
     * a schema type to see if it is an array type.  In order to be an array type
     * it must be a complex type which includes a sequence of a single element which
     * has it's minoccurs and/or maxoccures attributes set to values which denote an array.
     *
     * @param type Either a complexType or a simpleType node from the schema.
     * @return The QName of the array's item type, null if the type is not an array,
     */
    private QName getArrayItemType(Element type, WsdlTypes wsdlTypes) {

        if (type == null || "simpleElement".equals(type.getLocalName())) {
            return null;
        }

        Element sequence = DomUtils.getChildElementByName(type, "sequence");
        if (sequence != null) {
            return getArrayItemTypeFromSequence(sequence, wsdlTypes);
        }

        Element complexContent = DomUtils.getChildElementByName(type, "complexContent");
        if (complexContent != null) {
            return getArrayItemTypeFromComplexContent(complexContent, wsdlTypes);
        }

        return null;
    }

    /**
     * Get an array items xml type from a sequence element.
     *
     * @param sequenceElement Sequence element.
     * @param wsdlTypes       Wsdl types abstraction.
     * @return QName QName of the array item xml type.
     */
    private QName getArrayItemTypeFromSequence(Element sequenceElement, WsdlTypes wsdlTypes) {

        Element element = DomUtils.getChildElementByName(sequenceElement, "element");
        if (element == null) {
            return null;
        }
        if (!isArray(element)) {
            return null;
        }
        return wsdlTypes.getTypeQName(element.getAttribute("type"));
    }

    /**
     * Get an array items xml type from a complexContent element.
     *
     * @param ccElement Complex content element.
     * @param wsdlTypes Wsdl types abstraction.
     * @return QName QName of the array item xml type.
     */
    private QName getArrayItemTypeFromComplexContent(Element ccElement, WsdlTypes wsdlTypes) {

        Element restriction = DomUtils.getChildElementByName(ccElement, "restriction");
        if (restriction == null) {
            return null;
        }

        String base = restriction.getAttribute("base");
        if (!"soapenc:Array".equals(base)) {
            return null;
        }

        Element attribute = DomUtils.getChildElementByName(restriction, "attribute");
        if (attribute == null) {
            return null;
        }

        String arrayType = attribute.getAttribute("wsdl:arrayType");
        if (arrayType == null) {
            return null;
        }
        return wsdlTypes.getTypeQName(arrayType);
    }

    /**
     * Override the equals method.
     *
     * @param o Object to compare to.
     * @return true if equal
     */
    public boolean equals(Object o) {
        if (o instanceof WsdlOpParameter) {
            return _name.equals(((WsdlOpParameter) o).getName());
        }
        return false;
    }
}
